#!/usr/bin/python3

import argparse
import json
import os
import subprocess
import sys

from concurrent.futures import ProcessPoolExecutor
from glob import glob
from pathlib import Path
from typing import Any, List, Literal, Optional, Tuple

CapturedOutput = List[Tuple[Literal['stdout', 'stderr'], str]]


def parse_args():
    """
    Get command line arguments and process translation actions
    """
    parser = argparse.ArgumentParser()
    parser.description = 'Arguments for cinnamon-spices-makepot'
    parser.add_argument('-i', '--install', action='store_true',
                        help='Compiles and installs any .po files contained in'
                             ' a spices po folder.'
                             'Use this option to test your translations'
                             ' locally before uploading to Spices.')
    parser.add_argument('-r', '--remove', action='store_true',
                        help='The opposite of install, removes local '
                             'translations.')
    parser.add_argument('-a', '--all', action='store_true',
                        help='Create or update translation templates for all '
                             'Spices')
    parser.add_argument('uuid', type=str, metavar='UUID', nargs='?',
                        help='the UUID of the Spice')
    args = parser.parse_args()
    if args.uuid and args.uuid.endswith('/'):
        args.uuid = args.uuid.replace('/', '')
    if args.all and not args.uuid:
        folders = [file_path for file_path in os.listdir(
            '.') if os.path.isdir(file_path) and not file_path.startswith('.')]

        if args.install:
            for folder in folders:
                install_po(folder, True)
        elif args.remove:
            for folder in folders:
                remove_po(folder, True)
        else:
            with ProcessPoolExecutor() as executor:
                for output in executor.map(make_pot, folders):
                    print_output(output)

    elif args.install and args.remove:
        print('Only -i/--install OR -r/--remove may be specified. Not both.')
        sys.exit(1)
    elif args.install and args.uuid:
        install_po(args.uuid)
    elif args.remove and args.uuid:
        remove_po(args.uuid)
    elif args.uuid:
        output = make_pot(args.uuid)
        print_output(output)
    else:
        parser.print_help()


def print_output(output: CapturedOutput):
    """
    Replay the output of a command, preserving stdout/stderr order in the
    output list
    """
    # Flush is needed or we won't print in the right order
    for stream, content in output:
        file = sys.stdout if stream == 'stdout' else sys.stderr
        print(content, end='', file=file, flush=True)

    # Add a newline to the end of the output
    print()


def get_command_output(cmd: List[str], cwd: Optional[str] = None, check: bool = True, stderr: Optional[int] = None) -> CapturedOutput:
    """
    Gather command output while preserving stdout/stderr order and distinction

    Based on https://stackoverflow.com/a/56918582/14100077

    @param cmd: The command to run
    @param cwd: The working directory to run the command in
    @param check: Passed to subprocess.run when calling it

    @return: A list of tuples, where the first element is the stream
             ('stdout' or 'stderr') and the second element is the content
    """
    try:
        capture_output = stderr is None
        result = subprocess.run(cmd, cwd=cwd, check=check, stderr=stderr,
                                text=True, capture_output=capture_output)
        output: CapturedOutput = []
        if result.stdout:
            output.extend(('stdout', line)
                          for line in result.stdout.splitlines(keepends=True))
        if result.stderr:
            output.extend(('stderr', line)
                          for line in result.stderr.splitlines(keepends=True))
        return output
    except subprocess.CalledProcessError as e:
        return [('stderr', str(e))]


def install_po(uuid: str, _all: bool = False):
    """
    Install translation files locally from the po directory of the UUID
    """
    uuid_path = f'{uuid}/files/{uuid}'
    try:
        contents = os.listdir(uuid_path)
    except FileNotFoundError:
        print(f'Translations not found for: {uuid}')
        if not _all:
            sys.exit(1)
    home = os.path.expanduser('~')
    locale_inst = f'{home}/.local/share/locale'
    if 'po' in contents:
        po_dir = f'{uuid_path}/po'
        for file in os.listdir(po_dir):
            if file.endswith('.po'):
                lang = file.split('.')[0]
                locale_dir = os.path.join(locale_inst, lang, 'LC_MESSAGES')
                os.makedirs(locale_dir, mode=0o755, exist_ok=True)
                subprocess.run(['msgfmt', '-c', os.path.join(po_dir, file),
                               '-o', os.path.join(locale_dir, f'{uuid}.mo')],
                               check=True)


def remove_po(uuid: str, _all: bool = False):
    """
    Remove local translation files for the UUID
    """
    home = os.path.expanduser('~')
    locale_inst = f'{home}/.local/share/locale'
    uuid_mo_list = glob(f'{locale_inst}/**/{uuid}.mo', recursive=True)
    if not uuid_mo_list:
        print(f'No translation files found for: {uuid}')
        if not _all:
            sys.exit(1)
    for uuid_mo_file in uuid_mo_list:
        os.remove(uuid_mo_file)


def process_po(path_to_po: str, po_info: list, uuid: str) -> CapturedOutput:
    """
    Process existing .po files and return the output

    @param path_to_po: The path to the .po file to process
    @param po_info: The metadata to use for the .po file
    @param uuid: The UUID of the applet

    @return: A list of tuples, where the first element is the stream
             ('stdout' or 'stderr') and the second element is the content
    """
    po_path = Path(path_to_po)
    po_dir = str(po_path.parent)
    po_file = po_path.name
    po_lang = po_path.stem
    name, po_author, year = po_info

    commands = [
        ['msguniq', '-o', po_file, po_file],
        ['intltool-update', '-g', uuid, '-d', po_lang],
        ['msgattrib', '--no-obsolete', '-w', '79', '-o', po_file, po_file]
    ]

    output: CapturedOutput = [('stdout', f'{po_lang} ')]
    for cmd in commands:
        output.extend(get_command_output(cmd, cwd=po_dir))

    with open(path_to_po, 'r+', encoding='utf-8') as po:
        content = po.read()
        content = content.replace('SOME DESCRIPTIVE TITLE.', name)
        content = content.replace('FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.', f'{po_author}, {year}')
        content = content.replace('#: ', '#. ')
        po.seek(0)
        po.write(content)
        po.truncate()

    return output


def make_pot(uuid: str) -> CapturedOutput:
    """
    Make the translation template file for the UUID
    """

    output: CapturedOutput = []

    folder = Path(f'{os.getcwd()}/{uuid}')
    output_dir = folder.joinpath(f'files/{uuid}')
    po_dir = output_dir.joinpath('po')

    # Prepare the pot file
    pot_file = uuid + '.pot'
    pot_file_path = po_dir.joinpath(pot_file)

    if pot_file_path.exists():
        pot_file_path.unlink()
    elif not po_dir.exists():
        po_dir.mkdir()

    output += [('stdout', f'\nProcessing translation files for: {uuid}\n')]
    output += get_command_output(['cinnamon-xlet-makepot',
                                  '-o', pot_file_path, folder],
                                 check=True, cwd=folder)

    if os.path.exists(pot_file_path):
        pot_file_path.chmod(0o0644)
    else:
        return output

    # Extract translatable strings from glade
    glade_list = glob('**/*.glade', recursive=True, root_dir=output_dir)
    glade_list += glob('**/*.ui', recursive=True, root_dir=output_dir)
    for glade_file in glade_list:
        output += get_command_output(['xgettext', '-jL', 'Glade',
                                      '-o', pot_file_path, glade_file],
                                     check=True, cwd=output_dir)

    # Extract translatable strings from shell scripts
    shell_list = glob('**/*.sh', recursive=True, root_dir=output_dir)
    for shell_file in shell_list:
        output += get_command_output(['xgettext', '-jL', 'Shell',
                                      '-o', pot_file_path, shell_file],
                                     check=True, cwd=output_dir,
                                     stderr=subprocess.DEVNULL)

    # Get the metadata file
    metadata_file = f'{uuid}/files/{uuid}/metadata.json'
    metadata_found = False
    name = ''
    version = ''
    try:
        with open(metadata_file, encoding='utf-8') as meta:
            metadata: dict[str, Any] = json.load(meta)
        name = str(metadata.get('name', uuid)).upper()
        version = str(metadata.get('version', '1.0'))
        metadata_found = True
    except FileNotFoundError:
        output.append(('stdout', f'{uuid}: metadata.json not found\n'))

    if metadata_found and 'name' not in metadata:
        output.append(('stdout', f'{uuid}: name not found in metadata.json\n'))

    if metadata_found and 'version' not in metadata:
        output.append(
            ('stdout', f'{uuid}: version not found in metadata.json\n'))

    # Update the pot file with the metadata
    address = 'https://github.com/linuxmint/cinnamon-spices-applets/issues'
    output.extend(get_command_output(['xgettext', '-w', '79', '--foreign-user',
                                      '--package-name', uuid,
                                      '--msgid-bugs-address', address,
                                      '--package-version', version,
                                      '-o', pot_file_path, pot_file_path],
                                     check=True))

    # Update the pot file header with the additional metadata
    info_file = f'{uuid}/info.json'
    original_author = ''
    author = ''
    git_author = ''
    year = 2016
    try:
        with open(info_file, encoding='utf-8') as info:
            info: dict[str, Any] = json.load(info)
        original_author = str(info.get('original_author', ''))
        author = str(info.get('author', ''))
    except FileNotFoundError:
        pass

    try:
        result = subprocess.run(['git', 'log', '--follow', '--format=%an|%ad',
                                '--date=format:%Y', '--reverse', '--no-merges',
                                 str(folder)], capture_output=True, check=True,
                                text=True)
        if result.stdout:
            git_author, year = result.stdout.strip().splitlines()[0].split('|')
    except subprocess.CalledProcessError:
        pass

    for po_author in [original_author, author, git_author]:
        if po_author and po_author != 'none':
            break

    with open(pot_file_path, 'r+', encoding='utf-8') as po:
        content = po.read()
        content = content.replace('SOME DESCRIPTIVE TITLE.', name)
        content = content.replace('FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.', f'{po_author}, {year}')
        content = content.replace('YEAR-MO-DA HO:MI+ZONE', '')
        content = content.replace('FULL NAME <EMAIL@ADDRESS>', '')
        content = content.replace('LANGUAGE <LL@li.org>', '')
        content = content.replace('#: ', '#. ')
        po.seek(0)
        po.write(content)
        po.truncate()

    # Process the po files
    glob_path = f'{output_dir}/**/*.po'
    po_list = glob(glob_path, recursive=True)
    po_info = [name, po_author, year]
    for po_file in po_list:
        os.chmod(po_file, 0o0644)

    with ProcessPoolExecutor() as executor:
        for lines in executor.map(process_po, po_list, [po_info] * len(po_list), [uuid] * len(po_list)):
            output += lines

    return output


if __name__ == '__main__':
    parse_args()
